// Copyright 2012 Jay Young. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/**
 * @fileoverview
 *
 * A class that encapsulates the documentation info for a given JS source file.
 */

var fs = require('fs'),
    strings = require('ringo/utils/strings'),
    {JSDocInfo} = com.google.javascript.rhino,
    {ParameterizedType, IndexedType, UnionType, RecordType} =
        com.google.javascript.rhino.jstype;



/**
 * A class that encapsulates the documentation info for a given JS source file.
 *
 * @param {string} filename The name of the file being documented.
 * @param {string} rev The revision being documented.
 * @param {JSTypeRegistry} typeRegistry The type registry for this file.
 *
 * @constructor
 */
var DocInfo = exports.DocInfo = function(filename, rev, typeRegistry) {
  /**
   * @type {string}
   */
  this.filename = filename;
  
  /**
   * @type {string}
   */
  this.rev = rev;

  /**
   * A map of file-level tags to their content.  Eg., fileoverview, authors.
   * @type {Object}
   */
  this.fileDescriptor = Object.create(null);

  /**
   * A map of class names to their descriptors.  Clients should not directly
   * attach descriptors to this object, but rather get a reference
   * via DocInfo#getClass(name).
   *
   * @type {Object}
   */
  this.classes = Object.create(null);

  /**
   * A map of interface names to their descriptors.  Clients should not directly
   * attach descriptors to this object, but rather get a reference
   * via DocInfo#getInterface(name).
   *
   * @type {Object}
   */
  this.interfaces = Object.create(null);

  /**
   * A map of enums to their descriptors.  Clients should not directly
   * attach descriptors to this object, but rather get a reference
   * via DocInfo#getEnum(name).
   * @type {Object}
   */
  this.enums = Object.create(null);

  /**
   * A map of typedefs to their descriptors.  Clients should not directly
   * attach descriptors to this object, but rather get a reference
   * via DocInfo#getTypedef(name).
   * @type {Object}
   */
  this.typedefs = Object.create(null);

  /**
   * A map of namespaced global functions and variables.  Clients should not
   * directly attach descriptors to this object, but rather get 
   *
   * @type {Object}
   */
  this.globals = Object.create(null);

  /**
   * A map of aliases defined in a goog.scope wrapper.  If goog.scope() was not
   * used in this file, the map will contain no aliases.
   *
   * @type {Object.<string, string>}
   * @private
   */
  this.aliases_ = null;

  /**
   * The compiler's type registry for this file.
   *
   * @type {JSTypeRegistry}
   * @private
   */
  this.typeRegistry_ = typeRegistry;
};


/**
 * Takes a "docinfo.json" filename and re-hydrates the serialized DocInfo
 * object within into an actual instance.
 *
 * @param {string} filename The filename of the serialized DocInfo.
 * @return {DocInfo} The re-hydrated DocInfo instance.
 */
DocInfo.fromFile = function(filename) {
  var docInfo = JSON.parse(fs.read(filename), function(k, v) {
        if (k === '') {
          var docInfo = Object.create(DocInfo.prototype);
          for (var i in v) {
            if (Object.prototype.hasOwnProperty.call(v, i)) {
              docInfo[i] = v[i];
            }
          }

          return docInfo;
        }

        return v;
      });

  // This is deleted right after the file is initially crawled.  We don't need
  // it here anyway.
  delete docInfo.typeRegistry_;
  return docInfo;
};


/**
 * Declares a set of aliases defined in a goog.scope() wrapper.  This method
 * should only be called if there actually were aliases declared.
 *
 * @param {!Object.<string, string>} aliases A map of aliases to their fully
 *    qualified names.
 */
DocInfo.prototype.setGoogScopeAliases = function(aliases) {
  this.aliases_ = aliases;
};


/**
 * Check an identifier to determine if it contains an alias and, if so, expand
 * the alias to its full name.
 *
 * @param {string} name The name to check.
 * @return {string} The expanded name if the input contained an alias or the
 *    original string if not.
 */
DocInfo.prototype.expandAlias = function(name) {
  var aliases = this.aliases_;

  if (! aliases) {
    return name;
  }

  if (aliases[name]) {
    return aliases[name];
  }

  // If the name is namespaced (ie., contains a '.'), start tearing off
  // prefixes to see if some portion of the namespace is an alias.
  var n = name,
      dotAt = n.lastIndexOf('.');
  while (dotAt > -1) {
    n = name.substring(0, dotAt);

    if (aliases[n]) {
      return aliases[n] + name.substring(dotAt);
    }

    dotAt = n.lastIndexOf('.', dotAt);
  }

  return name;
};


/**
 * Return the file descriptor where fileoverview, author, and see references
 * are stored.
 *
 * @return {FileDescriptor} The file descriptor for this file.
 */
DocInfo.prototype.getFileDescriptor = function() {
  return this.fileDescriptor;
};


/**
 * Get the class descriptor for a given class name.  If no descriptor was
 * previously created for that class, a new one will be created and returned.
 *
 * @param {string} className The class.
 * @return {ClassDescriptor} The class descriptor for that name.
 */
DocInfo.prototype.getClass = function(className) {
  className = this.expandAlias(className);

  if (this.interfaces[className]) {
    throw Error('An interface with name "' + className + '" has already ' +
                'been registered.');
  }

  var desc = this.classes[className];
  if (! desc) {
    desc = this.classes[className] = {};
  }
  return desc;
};


/**
 * Checks whether the given name is an already-declared class name.
 *
 * @param {string} className The name to check.
 * @return {boolean} Whether or not the given name corresponds to a declared
 *    class.
 */
DocInfo.prototype.isDeclaredClass = function(className) {
  className = this.expandAlias(className);

  return !!this.classes[className];
};


/**
 * Checks wehtehr the givne name is an already-declared interface.
 *
 * @param {string} interfaceName The name to check.
 * @return {boolean} Whether or not the given name corresponds to a declared
 *    interface.
 */
DocInfo.prototype.isDeclaredInterface = function(interfaceName) {
  interfaceName = this.expandAlias(interfaceName);

  return !!this.interfaces[interfaceName];
};


/**
 * Get the class descriptor for a given interface.  If no descriptor was
 * previously created for that interface, a new one will be created and
 * returned.
 *
 * @param {string} className The class.
 * @return {ClassDescriptor} The class descriptor for that name.
 */
DocInfo.prototype.getInterface = function(className) {
  className = this.expandAlias(className);

  if (this.classes[className]) {
    throw Error('A class with name "' + className + '" has already been ' +
                'registered.');
  }

  var desc = this.interfaces[className];
  if (! desc) {
    desc = this.interfaces[className] = {};
  }
  return desc;
};


/**
 * Gets a ClassDescriptor for a given name.  The name can be either an interface
 * or a class.  If no class or interface was registered with the given name,
 * this method will return null.
 *
 * @param {string} className The class descriptor to retrieve.
 * @return {(null|ClassDescriptor)} The requested class descriptor.
 */
DocInfo.prototype.getClassDescriptor = function(className) {
  className = this.expandAlias(className);

  var desc = this.classes[className] || this.interfaces[className] || null;
  if (! desc) {
    throw new Error('No class named ' + className + ' declared.');
  }

  return desc;
};


/**
 * Get the descriptor for a given enum.  If no descriptor was previously
 * created for this enum, a new one will be created and returned.
 *
 * @param {string} name The qualified name of an enum.
 * @return {EnumDescriptor} The descriptor for the requested enum.
 */
DocInfo.prototype.getEnum = function(name) {
  name = this.expandAlias(name);

  var enumDesc = this.enums[name];
  if (! enumDesc) {
    this.enums[name] = enumDesc = {};
  }

  return enumDesc;
};


/**
 * Get the descriptor for a given typedef.  If no descriptor was previously
 * created for this type, a new one will be created and returned.
 *
 * @param {string} name The qualified name of the typedef.
 * @return {TypedefDescriptor} The descriptor for the requested typedef.
 */
DocInfo.prototype.getTypedef = function(name) {
  name = this.expandAlias(name);

  var typeDesc = this.typedefs[name];
  if (! typeDesc) {
    this.typedefs[name] = typeDesc = {};
  }

  return typeDesc;
};


/**
 * Resolve a JSTypeExpression to a string.
 *
 * @param {(JSTypeExpression|string)} typeExpr A type expression.
 * @return {string} The string representation of that type expression.
 *    Automatically expands any goog.scope() aliases.
 */
DocInfo.prototype.resolveType = function(typeExpr) {
  if (! typeExpr) {
    return null;
  }

  if (typeof typeExpr === 'string') {
    return typeExpr;
  }

  var type = typeExpr.evaluate(null, this.typeRegistry_),
      resolved = this.resolveJSType(type, typeExpr.isVarArgs());

  return this.expandAlias(resolved);
};


/**
 * Determine the type of a JSType and resolve it to a string.
 *
 * @param {JSType} type The type to resolve.
 * @param {boolean} isVarArgs Whether or not this is a variable argument type
 *    e.g., {...number} or {...Object}
 * @return {string} The type as a displayable string.
 */
DocInfo.prototype.resolveJSType = function(type, isVarArgs) {
  var typeStr;
  
  // The Closure Compiler parser automatically turns variable-length type
  // expressions into optional-argument union types.  For example:
  //   {...number} becomes {(number|undefined)}
  //
  // except when the type is <Any Type>, in which case it just keeps the
  // <Any Type> around and ignores the "...".

  if (type instanceof ParameterizedType || type instanceof IndexedType) {
    typeStr = this.resolveParameterizedType(type);
  }
  
  else if (type instanceof RecordType) {
    typeStr = this.resolveRecordType(type);
  }

  else if (type instanceof UnionType) {
    typeStr = this.resolveUnionType(type, isVarArgs);
  }

  else {
    typeStr = type.getDisplayName() || type.toString();
    if (isVarArgs) {
      // Convert "<Any Type>" to "...<Any Type>" for var_args.  This will
      // be changed to "...*" by the TypeLinker class.
      // print('Adding ... #1');
      typeStr = '...' + typeStr;
    }
  }

  return typeStr;
};


/**
 * Resolve a UnionType JSType.
 *
 * @param {UnionType} type The JSType object to be resolved which has already
 *    been determined to be a UnionType instance.
 * @param {boolean} isVarArgs Whether or not this is a variable argument type
 *    e.g., {...number} or {...Object}
 *
 * @return {string} The resolved type string.
 */
DocInfo.prototype.resolveUnionType = function(type, isVarArgs) {
  var iter = type.getAlternates().iterator(),
      types = [];

  while (iter.hasNext()) {
    var subType = this.resolveJSType(iter.next());
    
    if (! (isVarArgs && subType === 'undefined')) {
      types.push(subType);
    }
  }

  var typeStr;
  if (types.length === 1) {
    // Should only ever happen when removing an "undefined" from a var args
    // union, but check to make sure.

    // if (isVarArgs) { print('Adding ... #2'); }
    typeStr = isVarArgs ? ('...' + types[0]) : types[0];
  }
  else {
    var typeStr = '(' + types.join('|') + ')';
    if (isVarArgs) {
      // print('Adding ... #3');
      typeStr = '...' + typeStr;
    }
  }

  return typeStr;
};


/**
 * Resolve a ParameterizedType JSType.
 *
 * @param {ParameterizedType} type The JSType object to be resolved which has
 *    already been determined to be a ParameterizedType instance.
 * @return {string} The resolved type string.
 */
DocInfo.prototype.resolveParameterizedType = function(type) {
  var typeStr = type.getDisplayName(), // 'Object' or 'Array'
      indexType = type.getIndexType(),
      elType = type.getParameterType();

  if (indexType && elType) {
    typeStr += '.<' + this.resolveJSType(indexType) + ', ' +
               this.resolveJSType(elType) + '>';
  }
  else if (! indexType && elType) {
    typeStr += '.<' + this.resolveJSType(elType) + '>';
  }

  return typeStr;
};


/**
 * Resolve record types.
 *
 * @param {RecordType} type The JSType object to be resolved which has already
 *    been determined to be a RecordType instance.
 * @return {string} the resolved type string.
 */
DocInfo.prototype.resolveRecordType = function(type) {
  var props = type.getOwnPropertyNames(),
      iter = props.iterator(),
      propList = [];

  while (iter.hasNext()) {
    var propName = iter.next(),
        propType = type.getPropertyType(propName),
        prop = propName + ':' + this.resolveJSType(propType);

    propList.push(prop);
  }

  var typeStr = '{' + propList.join(',') + '}';
  return typeStr;
};


/**
 * Convert a Java-land JSDoc.Visibility value to a JS number.
 *
 * @param {JSDoc.Visibility} vis The visibility value to convert.
 * @return {DocInfo.Vis} The corresponding visibility number.
 */
DocInfo.visibility = DocInfo.prototype.visibility = function(vis) {
  if (vis === JSDocInfo.Visibility.PRIVATE) {
    return DocInfo.Vis.PRIVATE;
  }
  else if (vis === JSDocInfo.Visibility.PROTECTED) {
    return DocInfo.Vis.PROTECTED;
  }
  else if (vis === JSDocInfo.Visibility.PUBLIC) {
    return DocInfo.Vis.PUBLIC;
  }
  else {
    return DocInfo.Vis.INHERITED;
  }
};


/**
 * Corresponding JS-land visibility constants.
 *
 * @enum {number}
 */
DocInfo.Vis = {  
  INHERITED: 0,
  
  PRIVATE: 1,
  
  PROTECTED: 2,
  
  PUBLIC: 3
};


/**
 * Split a string at its new lines and insert tabs so that each line of the
 * text is indented.
 *
 * @param {string} text The text to indent.
 * @return {string} The indented text.
 */
function indentText(text) {
  text = text || '';
  return '\t' + text.split('\n').join('\n\t');
}